﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CitizenDiary.Sync.Contract;
using CitizenDiary.Sync.Shared;
using System.Data.SqlClient;
using System.ServiceModel;

namespace CitizenDiary.Sync.Service
{
    public class SyncService : ISyncService
    {
        private string _database = "CitizenDiary_Server";

        private SyncTokenProvider _tokenProvider;

        public SyncService()
            : this(new InProcCache())
        {
        }

        public SyncService(ICache cache)
        {
            _tokenProvider = new SyncTokenProvider(cache);
        }

        public HandshakeResponse Handshake(HandshakeRequest request)
        {
            var syncItems = new SynchronizationCollection();
            using (var conn = Helper.CreateConnection(_database))
            {
                using (var cmd = conn.CreateCommand())
                {
                    // retrieve the data on server side for future compare
                    var serverDiaryItems = new SynchronizationCollection();
                    cmd.CommandText = "SELECT * FROM DiaryItem";
                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            serverDiaryItems.Add((Guid)reader["ID"], new SynchronizationItem(reader));
                        }
                    }

                    // check from the device data to the server data
                    foreach (var deviceDiaryItem in request.DeviceItems)
                    {
                        if (serverDiaryItems.ContainsKey(deviceDiaryItem.Key))
                        {
                            var serverDiaryItem = serverDiaryItems[deviceDiaryItem.Key];
                            // data exist on server
                            // should detect if we need to update to server, or to device, or it's a conflict
                            if (serverDiaryItem.UpdatedVersion == deviceDiaryItem.Value.UpdatedVersion && serverDiaryItem.UpdatedDeviceID == deviceDiaryItem.Value.UpdatedDeviceID)
                            {
                                // the updated version and device id are the same between server and device this means the data didn't change
                                syncItems.Add(
                                    deviceDiaryItem.Key,
                                    new SynchronizationItem() { Type = SynchronizationType.Ignore });
                            }
                            else
                            {
                                // the data was different between the server and device 
                                // if it had been changed on device we need to detect the conflict, otherwise just update the device data
                                if (deviceDiaryItem.Value.HasChanged())
                                {
                                    // change made on device 
                                    if (deviceDiaryItem.Value.BasedDeviceID == serverDiaryItem.UpdatedDeviceID && deviceDiaryItem.Value.BasedVersion == serverDiaryItem.UpdatedVersion)
                                    {
                                        // the device data's based version and device id are equals to the server
                                        // this means no change made before thie sync by other device 
                                        // so we can update this data to server from device
                                        syncItems.Add(
                                            deviceDiaryItem.Key,
                                            new SynchronizationItem() { Type = SynchronizationType.Post });
                                    }
                                    else
                                    {
                                        // the base information on device was different to the server
                                        // this means others had changed the data before this sync
                                        // it's a conflict since this deivce and other device are all changed this data
                                        syncItems.Add(
                                            deviceDiaryItem.Key,
                                            new SynchronizationItem(serverDiaryItem, SynchronizationType.Conflict));
                                    }
                                }
                                else
                                {
                                    // no change made on device
                                    // just download the server data and update to the device
                                    syncItems.Add(
                                        deviceDiaryItem.Key,
                                        new SynchronizationItem() { Type = SynchronizationType.Get });
                                }
                            }
                        }
                        else
                        {
                            // server doesn't contains the item that exist on device
                            // it need to be upload and insert to server
                            syncItems.Add(
                                deviceDiaryItem.Key,
                                new SynchronizationItem() { Type = SynchronizationType.Put });
                        }
                    }

                    // check from the server data 
                    // only if it had been updated after the device last sync
                    // it should be updated to device
                    foreach (var serverDiaryItem in serverDiaryItems)
                    {
                        if (!request.DeviceItems.ContainsKey(serverDiaryItem.Key) && serverDiaryItem.Value.UpdatedOn > request.GetLastSyncOnDateTime())
                        {
                            syncItems.Add(
                                serverDiaryItem.Key,
                                new SynchronizationItem() { Type = SynchronizationType.Get });
                        }
                    }
                }
            }

            // generate and save the token
            var token = string.Empty;
            var syncKey = string.Empty;
            _tokenProvider.GetToken(syncItems, out token, out syncKey);
            // only need to return the items that in post, put and conflict to device
            // others (get) no need to go to the device since they will be downloaded to without device providing any information
            return new HandshakeResponse()
            {
                Token = token,
                SynchronizationKey = syncKey,
                SynchronizationItems = new SynchronizationCollection(
                    syncItems.Where(i =>
                        i.Value.Type == SynchronizationType.Conflict ||
                        i.Value.Type == SynchronizationType.Post ||
                        i.Value.Type == SynchronizationType.Put))
            };
        }

        public SynchronizeResponse Synchronize(SynchronizeRequest request)
        {
            var syncItems = _tokenProvider.MergeSyncItems(request.Token, request.SynchronizationKey, request.SynchronizationItems);
            var response = new SynchronizeResponse();
            using (var conn = Helper.CreateConnection(_database))
            {
                // start transaction for the sync operation
                var tran = conn.BeginTransaction();
                using (var cmd = conn.CreateCommand())
                {
                    cmd.Transaction = tran;
                    try
                    {
                        // based on the sync type to update the server data from device
                        foreach (var syncItem in syncItems)
                        {
                            switch(syncItem.Value.Type)
                            {
                                case SynchronizationType.Conflict:
                                    throw new FaultException(string.Format("The item (ID = {0}) is conflict type which is incorrect.", syncItem.Key));
                                case SynchronizationType.Post: // update to server
                                    cmd.CommandText = string.Format(
                                        "UPDATE DiaryItem SET Content = '{0}', UpdatedVersion = {1}, UpdatedDeviceID = '{2}', IsDeleted = '{4}', UpdatedOn = '{5}' WHERE ID = '{3}'",
                                        syncItem.Value.Content,
                                        syncItem.Value.UpdatedVersion,
                                        syncItem.Value.UpdatedDeviceID,
                                        syncItem.Key,
                                        syncItem.Value.IsDeleted,
                                        syncItem.Value.UpdatedOn);
                                    cmd.ExecuteNonQuery();
                                    break;
                                case SynchronizationType.Put: // insert to server
                                    cmd.CommandText = string.Format(
                                        "INSERT INTO DiaryItem (ID, Content, UpdatedVersion, UpdatedDeviceID, IsDeleted, UpdatedOn) VALUES ('{0}', '{1}', {2}, '{3}', '{4}', '{5}')",
                                        syncItem.Key,
                                        syncItem.Value.Content,
                                        syncItem.Value.UpdatedVersion,
                                        syncItem.Value.UpdatedDeviceID,
                                        syncItem.Value.IsDeleted,
                                        syncItem.Value.UpdatedOn);
                                    cmd.ExecuteNonQuery();
                                    break;
                                case SynchronizationType.Get: // insert to local
                                    cmd.CommandText = string.Format("SELECT * FROM DiaryItem WHERE ID = '{0}'", syncItem.Key);
                                    using (var reader = cmd.ExecuteReader())
                                    {
                                        if (reader.Read())
                                        {
                                            response.SynchronizationItems.Add(syncItem.Key, new SynchronizationItem(reader, syncItem.Value.Type));
                                        }
                                    }
                                    break;
                                default: // ignore
                                    break;
                            }
                        }
                        tran.Commit();
                        // retrieve the max update version for this device
                        cmd.CommandText = string.Format("SELECT MAX(UpdatedVersion) FROM DiaryItem WHERE UpdatedDeviceID = '{0}'", request.DeviceID);
                        var maxUpdVersionObject = cmd.ExecuteScalar();
                        if (maxUpdVersionObject == null || maxUpdVersionObject == DBNull.Value)
                        {
                            response.MaxUpdateVersion = 0;
                        }
                        else
                        {
                            response.MaxUpdateVersion = (long)maxUpdVersionObject;
                        }
                    }
                    catch
                    {
                        if (tran != null)
                        {
                            tran.Rollback();
                        }
                        throw;
                    }
                }
            }

            return response;
        }

        public PingResponse Ping(PingRequest request)
        {
            var response = new PingResponse();
            using (var conn = Helper.CreateConnection(_database))
            {
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandText = string.Format(
                        "SELECT Device.ID AS DeviceID, Device.Name AS DeviceName, " +
                        "Member.ID AS MemberID, Member.Email AS MemberEmail, " +
                        "Application.ID AS ApplicationID, Application.Name AS ApplicationName " +
                        "FROM Device " +
                        "LEFT JOIN Member ON Device.MemberID = Member.ID " +
                        "LEFT JOIN Application ON Device.ApplicationID = Application.ID " +
                        "WHERE Device.ID = '{0}'",
                        request.DeviceID);
                    using (var reader = cmd.ExecuteReader())
                    {
                        if (reader.Read())
                        {
                            response.Device = new KeyValuePair<Guid, string>((Guid)reader["DeviceID"], (string)reader["DeviceName"]);
                            response.Member = new KeyValuePair<Guid, string>((Guid)reader["MemberID"], (string)reader["MemberEmail"]);
                            response.Application = new KeyValuePair<Guid, string>((Guid)reader["ApplicationID"], (string)reader["ApplicationName"]);
                        }
                    }
                }
            }
            return response;
        }
    }
}
